有一天艾倫正在開發「超級約翰」這款遊戲,這是一個橫向捲軸遊戲,主角可以透過跳躍來踩死敵人,關卡的目標就是一路過關斬將並將最後的旗子升起來。艾倫正在開發最重要的跳躍,只要偵測到搖桿有按下“A”,主角就會跳一下。但是在遊戲結束時會有一段動畫,玩家這時候不能按“A”來跳躍。所以艾倫就寫了以下這段程式碼:
class Game {
private Status gameStatus;
private Player player;
public void onUpdateGameStatus(Status status) {
this.gameStatus = status;
}
public void onKeyDown(String key) {
if (key.equals("A")) {
if (gameStatus == Status.OVER) {
//Do nothing
//TODO 這邊怪怪的
} else {
player.jump();
}
}
}
}
艾倫越看越不滿意,所以又想了另一個解法:
class Game {
private Status gameStatus;
private Player player;
public void onUpdateGameStatus(Status status) {
this.gameStatus = status;
if (status == Status.OVER) {
player == null;
}
}
public void onKeyDown(String key) {
if (key.equals("A")) {
if (player != null) {
player.jump();
}
}
}
}
這樣還是很奇怪啊!為了不讓角色可以操作而硬給他 null ,而且角色也沒有真的消失了,只是在跑動畫,這樣要是遊戲有劇情發生時是否也要用一樣的技巧?劇情結束了再把 player
塞回去嗎?更不用說未來還有很多不同的情況要處理。好險,他想到了之前看過了一些 Design Pattern,也許有幾個可以派上用場!
如果讀者對物件導向開發有一定的經驗的話,相信你一定碰到不少次的 Null 判斷。也很常為了 NullPointerException 而苦惱著。又或者是學了這麼多 Design Pattern,用了之後發現有些時候還是會有髒髒的 Null 判斷,現實上沒有像大家介紹的一樣那麼美好,別喪氣!Null Object Pattern 可以為你帶來救贖。
Null Object Pattern 會有至少一個 real object,一個 null object,以及他們共通的繼承對象 abstract object,Client 會透過 abstract object 的介面來使用 real object 或是 null object。這種使用方式就是大家所熟悉的多型 (Polymorphism)
讓我們繼續艾倫的故事吧!艾倫想到了他可以將控制角色的邏輯放到 PlayerController
這個類別裡面,這樣一來就可以封裝 Player
的控制行為了。
這邊用 Java 來當例子:
interface PlayerController {
void onKeyDown(String key);
}
class PlayerControllerImpl implements PlayerController {
private Player player;
public PlayerControllerImpl(Player player) {
this.player = player;
}
public void onKeyDown(String key) {
if (key.equals("A")) {
player.jump();
} else if(key.equals("B")) {
player.dash();
}
}
}
class NullPlayerController implements PlayerController {
public void onKeyDown(String key) {
//Do nothing
}
}
這樣一來,只要在遊戲結束時切換 PlayerController
,剩下的就可以不用管了!
class Game {
private PlayerController playerController;
private Player player;
public void onUpdateGameStatus(Status status) {
if (status == Game.Over) {
//不管玩家按什麼都不會影響
playerController = new NullPlayerController();
} else {
...
}
}
public void onKeyDown(String key) {
//隨時都可以委派給 PlayerController,不用在這裡知道遊戲的狀態
playerController.onKeyDown(key);
}
}
在艾倫原本的設計中,需要知道目前的遊戲狀態才能決定遊戲角色是否要做相對應的行為,巢狀的 if-else 判斷就因此出現了。未來需求越加越多時,很有可能會再出現另外一層的 if-else 。
另外一個好處是程式碼被妥善的放到他應該關注的地方了。遊戲狀態一改變,就更換了 PlayerController 的實體,所以下一個閱讀這段程式碼的人馬上就會知道遊戲狀態與控制器之間的關係,而不需要藉由多層的 if-else 來去推敲這些複雜的關係。
我很喜歡這個 Pattern,他很優雅了解決空實作的問題,充份的運用了多型所帶來的好處。剛剛也提到了他通常會搭配其他 Pattern 一起出現,所以下次當你遇到一個註解寫著 //Do nothing
的時候就可以想想,是不是還有什麼概念可以再進一步去抽象出來(因為聞到了 Null Object Pattern 的味道),有可能可以變成 Strategy Pattern, State Pattern 或是 Command Pattern,但是也要注意,如果邏輯沒有很複雜的話,也不需要花太多時間在這上面,等到主要問題浮現時再去做因應的設計。今天的介紹就到這邊,感謝大家的收看!
作者:Yanbin